﻿using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Practices.EnterpriseLibrary.Data;
using System.ComponentModel;


namespace Microsoft.Practices.Repository
{
    /// <summary>
    /// Utility to Cast a DBValue to its correct type.
    /// <para>ToOptional implementations supports specifying a default value for DBNull.</para>
    /// <para>ToRequired implementation throws an exception for DBNull.</para>
    /// </summary>
    public static class CastDBValue
    {
        #region ToOptional
        /// <summary>
        /// Casts and returns non-null DB values to the indicated type
        /// For DBNull, returns defaultValue
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="value">Expects DB value like reader column value or output parameter</param>
        /// <param name="defaultValue">Whatever the caller wants returned when value is DBNull</param>
        public static T ToOptional<T>(object value, T defaultValue)
        {
            TypeCode targetTypeCode = ResolveTargetTypeCode<T>();
            TypeCode underlyingTypeCode = Type.GetTypeCode(value.GetType());

            if (Enum.Equals(targetTypeCode, underlyingTypeCode))
                return (value != DBNull.Value) ? (T)value : defaultValue;

            return (value != DBNull.Value) ? (T)Convert.ChangeType(value, targetTypeCode) : defaultValue;
        }


        /// <summary>
        /// Examines the <paramref name="T"/> <see cref="Type"/> and determines
        /// the appropriate <see cref="TypeCode"/>, regardless of whether
        /// or not <paramref name="T"/> is <see cref="Nullable"/> type.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private static TypeCode ResolveTargetTypeCode<T>()
        {
            const string NULLABLE = "System.Nullable";
            Type targetType = typeof(T);
            if (targetType.IsGenericType)
            {
                if (!targetType.FullName.Contains(NULLABLE))
                    throw new NotSupportedException("Only System.Nullable generic types are supported.");

                Type[] typeArguments = targetType.GetGenericArguments();
                const byte ARRAY_LENGTH = 1;
                const byte ARRAY_INDEX = 0;
                if (typeArguments.Length == ARRAY_LENGTH)
                    return Type.GetTypeCode(typeArguments[ARRAY_INDEX]);
            }

            return Type.GetTypeCode(typeof(T));
        }

        /// <summary>
        /// Locates the value in specificed column of the reader
        /// If the value is DBNull, returns defaultValue
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="reader">reader with a row</param>
        /// <param name="columnName">valid column in the reader</param>
        /// <param name="defaultValue">Whatever the caller wants returned when value is DBNull</param>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public static T ToOptional<T>(IDataReader reader, String columnName, T defaultValue)
        {
            int columnIndex = reader.GetOrdinal(columnName);
            var columnValue = reader.GetValue(columnIndex);

            try
            {
                return ToOptional<T>(columnValue, defaultValue);
            }
            catch (InvalidCastException ex)
            {
                String targetTypeFullName = typeof(T).FullName;
                TypeCode underlyingSystemTypeCode = Type.GetTypeCode(reader.GetFieldType(columnIndex));
                throw new InvalidCastException(
                    String.Format("Trying to cast to {0} but type returned from database is {1}",
                        targetTypeFullName, Enum.GetName(typeof(TypeCode), underlyingSystemTypeCode)),
                    ex);
            }
            catch (OverflowException ex)
            {
                String targetTypeFullName = typeof(T).FullName;
                TypeCode underlyingSystemTypeCode = Type.GetTypeCode(reader.GetFieldType(columnIndex));
                throw new OverflowException(
                    String.Format("Trying to cast to {0} but type returned from database is {1}",
                        targetTypeFullName, Enum.GetName(typeof(TypeCode), underlyingSystemTypeCode)),
                    ex);
            }
        }

        /// <summary>
        /// Locates the value in specificed parameter of the database and command
        /// If the value is DBNull, returns defaultValue
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="db">valid database</param>
        /// <param name="command">valid command</param>
        /// <param name="parameterName">parameter that exists in the command</param>
        /// <param name="defaultValue">Whatever the caller wants returned when value is DBNull</param>
        public static T ToOptional<T>(Database db, DbCommand command, string parameterName, T defaultValue)
        {
            return ToOptional<T>(db.GetParameterValue(command, parameterName), defaultValue);
        }
        #endregion

        #region ToRequired
        /// <summary>
        /// Casts and returns non-null DB values to the indicated type
        /// For DBNull, throws an exception with errorMessage
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="value">Expects DB value like reader column value or output parameter</param>
        /// <param name="fieldName">Name of the db column or parameter to be passed to the exception when value is DBNull</param>
        /// <param name="errorMessage">Whatever the caller wants sent as the message to the exception when value is DBNull</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public static T ToRequired<T>(Object value, string fieldName, string errorMessage)
        {
            if (value == DBNull.Value)
                throw new DBValueNullException(fieldName, errorMessage);

            TypeCode targetTypeCode = ResolveTargetTypeCode<T>(); ;
            TypeCode underlyingTypeCode = Type.GetTypeCode(value.GetType());

            if (Enum.Equals(targetTypeCode, underlyingTypeCode))
                return (T)value;

            return (T)Convert.ChangeType(value, targetTypeCode);
        }

        /// <summary>
        /// Locates the value in the specificed column of the reader
        /// If the value is DBNull, throws an exception indicating the column name
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="reader">reader with a row</param>
        /// <param name="columnName">valid column in the reader</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public static T ToRequired<T>(IDataReader reader, String columnName)
        {
            return ToRequired<T>(reader, columnName, string.Empty);
        }

        /// <summary>
        /// Locates the value in the specificed column of the reader
        /// If the value is DBNull, throws an exception indicating the column name and errorMessage
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="reader">reader with a row</param>
        /// <param name="columnName">valid column in the reader</param>
        /// <param name="errorMessage">
        /// Error message to be appended to the column name if value found is null
        /// (e.g.) "ClientID for current records is 12345"</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public static T ToRequired<T>(IDataReader reader, String columnName, string errorMessage)
        {
            int columnIndex = reader.GetOrdinal(columnName);
            var columnValue = reader.GetValue(columnIndex);
            
            try
            {
                return ToRequired<T>(columnValue, columnName, errorMessage);
            }
            catch (InvalidCastException ex)
            {
                String targetTypeFullName = typeof(T).FullName;
                TypeCode underlyingSystemTypeCode = Type.GetTypeCode(reader.GetFieldType(columnIndex));
                throw new InvalidCastException(
                    String.Format("Trying to cast to {0} but type returned from database is {1}",
                        targetTypeFullName, Enum.GetName(typeof(TypeCode), underlyingSystemTypeCode)),
                    ex);
            }
            catch (OverflowException ex)
            {
                String targetTypeFullName = typeof(T).FullName;
                TypeCode underlyingSystemTypeCode = Type.GetTypeCode(reader.GetFieldType(columnIndex));
                throw new OverflowException(
                    String.Format("Trying to cast to {0} but type returned from database is {1}",
                        targetTypeFullName, Enum.GetName(typeof(TypeCode), underlyingSystemTypeCode)),
                    ex);
            }
        }

        /// <summary>
        /// Locates the value in specificed parameter of the database and command
        /// If the value is DBNull, throws an exception indicating the parameter name
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="db">valid database</param>
        /// <param name="command">valid command</param>
        /// <param name="parameterName">parameter that exists in the command</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public static T ToRequired<T>(Database db, DbCommand command, string parameterName)
        {
            return ToRequired<T>(db, command, parameterName, string.Empty);
        }

        /// <summary>
        /// Locates the value in specificed parameter of the database and command
        /// If the value is DBNull, throws an exception indicating the parameter name and error message
        /// else casts the value to the indicated type
        /// </summary>
        /// <typeparam name="T">Expects simple types like string, date, int, etc.</typeparam>
        /// <param name="db">valid database</param>
        /// <param name="command">valid command</param>
        /// <param name="parameterName">parameter that exists in the command</param>
        /// <param name="errorMessage">
        /// Error message to be appended to the column name if value found is null
        /// (e.g.) "Search criteria for current command is SSN = 123456789"</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
        public static T ToRequired<T>(Database db, DbCommand command, string parameterName, string errorMessage)
        {
            return ToRequired<T>(db.GetParameterValue(command, parameterName), parameterName, errorMessage);
        }
        #endregion

        #region ToRequired for string
        /// <summary>
        /// Casts and returns non-null DB values to string.
        /// For DBNull, throws an exception with errorMessage.
        /// When allowEmptyString is false and value evaluates to String.Empty,
        /// an exception is thrown just like when it is DBNull.
        /// </summary>
        /// <param name="value">expected to evaluate to a string</param>
        /// <param name="fieldName">Name of the db column or parameter to be passed to the exception when value is DBNull</param>
        /// <param name="allowEmptyString">When false, indicates that String.Empty should be treated like DBNull</param>
        /// <param name="errorMessage">Whatever the caller wants sent as the message to the exception when value is null</param>
        /// <exception cref="DBValueNullException"/>
        public static string ToRequired(object value, string fieldName, bool allowEmptyString, string errorMessage)
        {
            string result = ToRequired<string>(value, fieldName, errorMessage);

            if (allowEmptyString || result.Length > 0)
                return result;

            throw new DBValueNullException(fieldName, errorMessage);
        }
        /// <summary>
        /// Locates the value in the specificed column of the reader and casts it to a string.
        /// If the value is DBNull, throws an exception indicating the column name.
        /// When allowEmptyString is false and the value evaluates to String.Empty,
        /// an exception is thrown just like when it is DBNull.
        /// </summary>
        /// <param name="reader">reader with a row</param>
        /// <param name="columnName">valid column in the reader</param>
        /// <param name="allowEmptyString">When false, indicates that String.Empty should be treated like DBNull</param>
        /// <exception cref="DBValueNullException"/>
        public static string ToRequired(IDataReader reader, String columnName, bool allowEmptyString)
        {
            return ToRequired(reader, columnName, allowEmptyString, string.Empty);
        }

        /// <summary>
        /// Locates the value in the specificed column of the reader and casts it to a string
        /// If the value is DBNull, throws an exception indicating the column name and errorMessage
        /// When allowEmptyString is false and the value evaluates to String.Empty,
        /// an exception is thrown just like when it is DBNull.
        /// </summary>
        /// <param name="reader">reader with a row</param>
        /// <param name="columnName">valid column in the reader</param>
        /// <param name="allowEmptyString">When false, indicates that String.Empty should be treated like DBNull</param>
        /// <param name="errorMessage">Whatever the caller wants sent as the message to the exception when value is null</param>
        /// <exception cref="DBValueNullException"/>
        [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
        public static string ToRequired(IDataReader reader, String columnName, bool allowEmptyString, string errorMessage)
        {
            int columnIndex = reader.GetOrdinal(columnName);

            return ToRequired(reader.GetValue(columnIndex), columnName, allowEmptyString, errorMessage);
        }

        /// <summary>
        /// Locates the value in specificed parameter of the database and command and casts it to a string
        /// If the value is DBNull, throws an exception indicating the parameter name
        /// When allowEmptyString is false and the value evaluates to String.Empty,
        /// an exception is thrown just like when it is DBNull.
        /// </summary>
        /// <param name="db">valid database</param>
        /// <param name="command">valid command</param>
        /// <param name="parameterName">parameter that exists in the command</param>
        /// <param name="allowEmptyString">When false, indicates that String.Empty should be treated like DBNull</param>
        public static string ToRequired(Database db, DbCommand command, string parameterName, bool allowEmptyString)
        {
            return ToRequired<string>(db, command, parameterName, string.Empty);
        }

        /// <summary>
        /// Locates the value in specificed parameter of the database and command and casts it to a string
        /// If the value is DBNull, throws an exception indicating the parameter name and error message
        /// When allowEmptyString is false and the value evaluates to String.Empty,
        /// an exception is thrown just like when it is DBNull.
        /// </summary>
        /// <param name="db">valid database</param>
        /// <param name="command">valid command</param>
        /// <param name="parameterName">parameter that exists in the command</param>
        /// <param name="allowEmptyString">When false, indicates that String.Empty should be treated like DBNull</param>
        /// <param name="errorMessage">
        /// Error message to be appended to the parameter name if value found is null
        /// (e.g.) "Search criteria for current command is SSN = 123456789"</param>
        public static string ToRequired(Database db, DbCommand command, string parameterName, bool allowEmptyString, string errorMessage)
        {
            return ToRequired<string>(db.GetParameterValue(command, parameterName), parameterName, errorMessage);
        }
        #endregion
    }
}
